package org.junyee.demo.security.ec;

import java.security.InvalidKeyException;
import org.junyee.demo.security.exception.KeyException;
import java.security.KeyPair;
import java.security.Signature;
import java.security.SignatureException;
import java.security.interfaces.ECPrivateKey;
import java.security.interfaces.ECPublicKey;
import java.security.spec.ECFieldFp;
import java.util.Arrays;
import java.math.BigInteger;
import org.apache.tomcat.util.buf.HexUtils;
import org.springframework.util.Assert;
import org.springframework.util.Base64Utils;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;

import lombok.Getter;
import lombok.ToString;
import lombok.extern.slf4j.Slf4j;

/**
 * JDK > 1.8 and SecurityProvider is SunEC The SunEC is simple provider. Such as
 * create ECC Key, Signature. But openssl pem and chiper is not supported. Or
 * You can use BC(BouncyCastle) to develop ECC, support openssl pem , chiper and
 * other advance function. If you want to develop JWK/JWS/JWT, you can use java
 * JOSE (Jose4J, you can get it through link: https://jwt.io). JOSE implement
 * OIDC support ECC/RSA. When I was develop this ECC demo, the spring boot has
 * not integrate ES JWKS. you can develop an ES JWKS starter based the Jose4J by
 * yourself.
 */
@Getter
@ToString
@Slf4j
public class EcKey {

    private final String type;

    private BigInteger d;
    private BigInteger x;
    private BigInteger y;

    private ECPrivateKey ecPrivateKey;
    private ECPublicKey ecPublicKey;

    private EcEs ecEs;
    private EcPem ecPem;

    private EcKey(String type) {
        this.type = type;
    }

    private EcKey(BigInteger d, BigInteger x, BigInteger y, String type) {
        this(type);
        this.d = d;
        this.x = x;
        this.y = y;
    }

    public static EcKey generate(EcEs ecEs) {
        Assert.notNull(ecEs, "ecEs Required");
        KeyPair genKeyPair = ecEs.generateKeyPair();
        ECPrivateKey private1 = (ECPrivateKey) genKeyPair.getPrivate();
        ECPublicKey public1 = (ECPublicKey) genKeyPair.getPublic();
        if (log.isDebugEnabled()) {
            // print these to assemble custom EcParameterSpec
            log.debug("p: {}", ((ECFieldFp) (private1.getParams().getCurve().getField())).getP()); // print P
            log.debug("a: {}", private1.getParams().getCurve().getA()); // ec param spec curve a
            log.debug("b: {}", private1.getParams().getCurve().getB()); // ec param spec curve b
            log.debug("x: {}", private1.getParams().getGenerator().getAffineX()); // public key ec point x big num
            log.debug("y: {}", private1.getParams().getGenerator().getAffineY()); // public key ec point y big num
            log.debug("n: {}", private1.getParams().getOrder()); // ec param spec order n
            log.debug("h: {}", private1.getParams().getCofactor());// ec param spec cofactor h
        }

        EcKey ecFactor = new EcKey(private1.getS(), public1.getW().getAffineX(), public1.getW().getAffineY(), "ES");
        ecFactor.ecPrivateKey = private1;
        ecFactor.ecPublicKey = public1;
        ecFactor.ecEs = ecEs;
        ecFactor.ecPem = EcPem.build(private1, public1);
        return ecFactor;
    }

    public static EcKey createKeyFromPkcs8Pem(String pem) {
        EcPem buildFromPrivateKeyPem = EcPem.buildFromPem(pem);
        EcKey ecFactor = new EcKey("PEM");
        ecFactor.d = buildFromPrivateKeyPem.getPrivateKey() != null ? buildFromPrivateKeyPem.getPrivateKey().getS()
                : null;
        ecFactor.x = buildFromPrivateKeyPem.getPublicKey() != null
                ? buildFromPrivateKeyPem.getPublicKey().getW().getAffineX()
                : null;
        ecFactor.y = buildFromPrivateKeyPem.getPublicKey() != null
                ? buildFromPrivateKeyPem.getPublicKey().getW().getAffineY()
                : null;
        ecFactor.ecPrivateKey = buildFromPrivateKeyPem.getPrivateKey();
        ecFactor.ecPublicKey = buildFromPrivateKeyPem.getPublicKey();
        ecFactor.ecPem = buildFromPrivateKeyPem;
        return ecFactor;
    }

    public static EcKey createKey(byte[] db, byte[] xb, byte[] yb, EcEs ecEs) {
        Assert.notNull(ecEs, "EC ES Required");
        boolean notPublic = ObjectUtils.isEmpty(xb) || ObjectUtils.isEmpty(yb);
        boolean notPrivate = ObjectUtils.isEmpty(db);
        if (notPublic && notPrivate) {
            throw new KeyException("Can not create any key.");
        }
        BigInteger x = notPublic ? null : new BigInteger(1, xb), y = notPublic ? null : new BigInteger(1, yb),
                d = notPrivate ? null : new BigInteger(1, db);
        EcKey ecFactor = new EcKey(d, x, y, "ES");
        if (!notPrivate) {
            ecFactor.ecPrivateKey = ecEs.createPrivateKey(d);
        }
        if (!notPublic) {
            ecFactor.ecPublicKey = ecEs.createPublicKey(x, y);
        }
        ecFactor.ecEs = ecEs;
        ecFactor.ecPem = EcPem.build(ecFactor.ecPrivateKey, ecFactor.ecPublicKey);
        return ecFactor;
    }

    public boolean hasPublicKey() {
        return x != null && y != null && ecPublicKey != null;
    }

    public boolean hasPrivateKey() {
        return d != null && ecPrivateKey != null;
    }

    public Signature createSignature() {
        if (ecEs == null && ecPem == null) {
            throw new KeyException("Can not create Signature");
        }
        return ecEs == null ? ecPem.generateSignature() : ecEs.generateSignature();
    }

    public byte[] sign(byte[] content) {
        Assert.notNull(content, "Content Required");
        if (!hasPrivateKey()) {
            throw new KeyException("Has not private key to sign.");
        }
        Signature signatureSign = createSignature();
        try {
            signatureSign.initSign(ecPrivateKey);
            signatureSign.update(content);
            return signatureSign.sign();
        } catch (InvalidKeyException | SignatureException e) {
            throw new KeyException(e);
        }
    }

    public boolean verify(byte[] signatureContent, byte[] content) {
        Assert.notNull(signatureContent, "SignatureContent Required");
        Assert.notNull(content, "Content Required");

        if (!hasPublicKey()) {
            throw new KeyException("Has not public key to verification.");
        }
        Signature signatureVerify = createSignature();
        try {
            signatureVerify.initVerify(ecPublicKey);
            signatureVerify.update(content);
            return signatureVerify.verify(signatureContent);
        } catch (InvalidKeyException | SignatureException e) {
            throw new KeyException(e);
        }
    }

    public byte[] signES(byte[] content) {
        byte[] derSignature = sign(content);
        if (log.isDebugEnabled()) {
            log.debug("Signature: {}", StringUtils.arrayToDelimitedString(Arrays.asList(derSignature).toArray(), ","));
        }

        return EcUtils.transcodeSignatureToConcat(derSignature);
    }

    public boolean verifyES(byte[] esSignature, byte[] content) {
        byte[] transcodeSignatureToDER = EcUtils.transcodeSignatureToDER(esSignature);
        return verify(transcodeSignatureToDER, content);
    }

    public String getBase64URLSafeX() {
        return x == null ? null : encodeToUrlSafeString(x.toByteArray());
    }

    public String getBase64URLSafeY() {
        return y == null ? null : encodeToUrlSafeString(y.toByteArray());
    }

    public String getBase64URLSafeD() {
        return d == null ? null : encodeToUrlSafeString(d.toByteArray());
    }

    public static EcKey createPrivateKeyFromUrlSafeBase64(String dDecoder, String xDecoder, String yDecoder,
            EcEs ecEs) {
        Assert.hasText(dDecoder, "dDecoder must be has text!");
        Assert.hasText(xDecoder, "xDecoder must be has text!");
        Assert.hasText(yDecoder, "yDecoder must be has text!");

        byte[] d = Base64Utils.decodeFromUrlSafeString(dDecoder);
        byte[] x = Base64Utils.decodeFromUrlSafeString(xDecoder);
        byte[] y = Base64Utils.decodeFromUrlSafeString(yDecoder);
        return createKey(d, x, y, ecEs);
    }

    public static EcKey createPublicKeyFromUrlSafeBase64(String xDecoder, String yDecoder, EcEs ecEs) {
        Assert.hasText(xDecoder, "XDecoder must be has text!");
        Assert.hasText(yDecoder, "YDecoder must be has text!");

        byte[] x = Base64Utils.decodeFromUrlSafeString(xDecoder);
        byte[] y = Base64Utils.decodeFromUrlSafeString(yDecoder);
        return createKey(null, x, y, ecEs);
    }

    /**
     * Caution: automation recognition ECParameterSpec maybe hide bug if you test a
     * changeless jwk successfully , you can call the method.
     * 
     * @param db
     * @param xb
     * @param yb
     * @return
     */
    public static EcKey createKey(byte[] db, byte[] xb, byte[] yb) {
        byte[] bytes = ObjectUtils.isEmpty(db) ? ObjectUtils.isEmpty(xb) ? yb : xb : db;
        if (ObjectUtils.isEmpty(bytes)) {
            throw new KeyException("Can not create any key.");
        }
        return createKey(db, xb, yb, EcEs.getByBigInteger(new BigInteger(1, bytes)));
    }

    public static EcKey createPrivateKeyFromHex(String dHex, String xHex, String yHex, EcEs ecEs) {
        Assert.hasText(dHex, "DHex must be has text!");
        Assert.hasText(xHex, "XHex must be has text!");
        Assert.hasText(yHex, "YHex must be has text!");

        byte[] d = HexUtils.fromHexString(dHex);
        byte[] x = HexUtils.fromHexString(xHex);
        byte[] y = HexUtils.fromHexString(yHex);
        return createKey(d, x, y, ecEs);
    }

    public static EcKey createPublicKeyFromHex(String xHex, String yHex, EcEs ecEs) {
        Assert.hasText(xHex, "XHex must be has text!");
        Assert.hasText(yHex, "YHex must be has text!");

        byte[] x = HexUtils.fromHexString(xHex);
        byte[] y = HexUtils.fromHexString(yHex);
        return createKey(null, x, y, ecEs);
    }

    private String encodeToUrlSafeString(byte[] bytes) {
        String text;
        int index;
        return bytes == null ? null
                : (index = (text = Base64Utils.encodeToUrlSafeString(bytes)).indexOf('=')) > 0
                        ? text.substring(0, index)
                        : text;
    }

    /*
     * public static EcFactor createPrivateKeyFromBase64(String dDecoder, String
     * xDecoder, String yDecoder, EcEs ecEs) { Assert.hasText(dDecoder,
     * "dDecoder must be has text!"); Assert.hasText(xDecoder,
     * "xDecoder must be has text!"); Assert.hasText(yDecoder,
     * "yDecoder must be has text!");
     * 
     * byte[] d = Base64Utils.decodeFromString(dDecoder); byte[] x =
     * Base64Utils.decodeFromString(xDecoder); byte[] y =
     * Base64Utils.decodeFromString(yDecoder); return createKey(d, x, y, ecEs); }
     * 
     * public static EcFactor createPublicKeyFromBase64(String xDecoder, String
     * yDecoder, EcEs ecEs) { Assert.hasText(xDecoder,
     * "XDecoder must be has text!"); Assert.hasText(yDecoder,
     * "YDecoder must be has text!");
     * 
     * byte[] x = Base64Utils.decodeFromString(xDecoder); byte[] y =
     * Base64Utils.decodeFromString(yDecoder); return createKey(null, x, y, ecEs); }
     * 
     * public static EcFactor createPrivateKeyFromUrlSafeBase64(String dDecoder,
     * String xDecoder, String yDecoder) { Assert.hasText(dDecoder,
     * "dDecoder must be has text!"); Assert.hasText(xDecoder,
     * "xDecoder must be has text!"); Assert.hasText(yDecoder,
     * "yDecoder must be has text!");
     * 
     * byte[] d = Base64Utils.decodeFromUrlSafeString(dDecoder); byte[] x =
     * Base64Utils.decodeFromUrlSafeString(xDecoder); byte[] y =
     * Base64Utils.decodeFromUrlSafeString(yDecoder); return createKey(d, x, y); }
     * 
     * public static EcFactor createPublicKeyFromUrlSafeBase64(String xDecoder,
     * String yDecoder) { Assert.hasText(xDecoder, "XDecoder must be has text!");
     * Assert.hasText(yDecoder, "YDecoder must be has text!");
     * 
     * byte[] x = Base64Utils.decodeFromUrlSafeString(xDecoder); byte[] y =
     * Base64Utils.decodeFromUrlSafeString(yDecoder); return createKey(null, x, y);
     * }
     */

}
